package com.wz.utils.concurrency.threadpool;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Leetcode 1242. 多线程网页爬虫
 * // This is the HtmlParser's API interface.
 * // You should not implement it, or speculate about its implementation
 * interface HtmlParser {
 * public List<String> getUrls(String url) {}
 * }
 */
class Solution {

    /**
     * 已知URL集合，存储已遍历的所有URL
     */
    private HashSet<String> totalUrls = new HashSet<>(32);

    /**
     * 结果URL队列及对应的锁
     */
    private ReentrantLock resultLock = new ReentrantLock();
    private LinkedList<String> resultUrls = new LinkedList<>();

    /**
     * 待抓取的URL队列以及对应的锁
     */
    private ReentrantLock crawLock = new ReentrantLock();
    private Queue<String> ToCrawlUrls = new LinkedBlockingDeque<>();

    /**
     * 正在执行的工作线程个数，作为退出条件
     */
    private AtomicInteger choreCount = new AtomicInteger(0);

    /**
     * 主线程的方法
     * 仔细思考一下，这个有点像广度优先搜索，遍历到一个url节点，这个url就被访问过，如果符合条件就放入结果url队列中
     * 将涉及到的全部符合要求的url加入待抓取的队列，在下一层（循环、递归或者这里的新的线程中从队列去拿）
     * 关键点在于线程的退出，也就是广搜的优先队列为空，这里就是加上所有的工作线程都执行完毕，或者去关闭线程池
     *
     * @param startUrl   url启动
     * @param htmlParser html解析器
     * @return
     */
    public List<String> crawl(String startUrl, HtmlParser htmlParser) {
        String hostName = extractHostName(startUrl);
        //存储已知url
        this.totalUrls.add(startUrl);
        //该url符合条件，放入结果集
        addUrlToResult(startUrl);
        //以该url为起点继续爬
        addUrlToCrawl(startUrl);

        //region 自己创建线程并且使用线程计数器判断是否超时
        AtomicInteger i = new AtomicInteger(0);
        //todo 主线程阻塞，不断循环一直到满足退出条件（1、无法再爬下去，2、正在工作的线程已经结束工作）
        while (true) {
            String urlToCrawl = fetchUrlToCrawl();
            if (urlToCrawl != null) {
                incrChore();
                Chore chore = new Chore(this, hostName, htmlParser, urlToCrawl);
                (new Thread(chore)).start();
                i.incrementAndGet();
            } else {
                if (this.choreCount.get() == 0) {
                    break;
                }
                LockSupport.parkNanos(1L);
            }
        }
        System.out.println("这是开启了多少个线程" + i);
        //endregion

        //region 线程池，但是题目会报超时
        //线程数太低也会导致超时，关键是核心线程数，有新任务则生成新线程处理，多的就扔队列，队列都放不下就使用100的空间，同时线程的也会大于核心线程数字，如果有空闲的线程，小于max，也会拿来用
        ExecutorService executor = new ThreadPoolExecutor(150, 150, 0L, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(200), new ThreadPoolExecutor.AbortPolicy());
        //todo 主线程阻塞，不断循环一直到满足退出条件（1、无法再爬下去，2、正在工作的线程已经结束工作）
        while (true) {
            //从待爬集中拿一个来爬
            String urlToCrawl = fetchUrlToCrawl();
            if (urlToCrawl == null) {
                break;
            }
            //增加一个工作线程的计数，然后实例化一个线程去爬这个url
            //incrChore();
            Chore chore = new Chore(this, hostName, htmlParser, urlToCrawl);
            executor.execute(chore);
        }

        //线程池不会马上退出，会知道添加到线程池中的人物都已经完成才会退出，因此需要阻塞来判断是否真的已经关闭,
        executor.shutdown();
        //阻塞直到线程都关闭
        while (true) {
            if (executor.isShutdown()) {
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //endregion

        return fetchResultUrls();
    }

    /**
     * 多线程调用的实例的类
     * 该类实现爬url的方法，将该实例作为线程的参数传入，让线程去执行该实例的方法，由于多个实例操作一份Solution本地变量，因此需要加锁
     */
    private class Chore implements Runnable {
        /**
         * Solution当前实例，可以利用当前实例的方法来处理，同时也可以获取集合，本质上这个类是抽出处理url的代码，让后让多个线程来跑
         */
        private Solution solution;
        private String hostName;
        private HtmlParser htmlParser;
        /**
         * //待爬url
         */
        private String urlToCrawl;

        Chore(Solution solution, String hostName, HtmlParser htmlParser, String urlToCrawl) {
            this.solution = solution;
            this.hostName = hostName;
            this.htmlParser = htmlParser;
            this.urlToCrawl = urlToCrawl;
        }

        @Override
        public void run() {
            try {
                //过滤url，也就是处理url，根据待爬url找到关联的其他url
                filterUrls(this.htmlParser.getUrls(urlToCrawl));
            } finally {
                //处理完这个url后，这个线程执行完毕，减少一个工作线程的计数,注意包装起来，使用原子类的方法
                this.solution.decrChore();
            }
        }

        /**
         * 处理url
         *
         * @param crawledUrls
         */
        private void filterUrls(List<String> crawledUrls) {
            if (crawledUrls == null || crawledUrls.isEmpty()) {
                return;
            }
            //遍历处理找到的其他url
            for (String url : crawledUrls) {
                //该URL在已知的URL集合中已存在，那么不需要再重复抓取，跳过（即该url访问过）
                if (this.solution.totalUrls.contains(url)) {
                    continue;
                } else {
                    //放入已知URL集合中，表示访问过
                    this.solution.totalUrls.add(url);
                }

                //这个url虽然才访问，但是hostName不符合要求，不用继续访问了
                String crawlHostName = this.solution.extractHostName(url);
                if (!crawlHostName.equals(this.hostName)) {
                    //如果抓取到的URL对应的HostName和StartUrl对应的hostname不同，直接丢弃
                    continue;
                }

                //将该url添加至结果链表，这个是符合要求的访问到的url，同时要根据这个url继续往下找（网络访问关联的url）
                this.solution.addUrlToResult(url);
                //将该url添加至待抓取链表，以便进行下一跳抓取，再其他的线程中会拿出来
                this.solution.addUrlToCrawl(url);
            }
        }
    }


    //region 其他方法
    /**
     * 获取URL的名字
     *
     * @param url
     * @return
     */
    private String extractHostName(String url) {
        //exclude "http://"
        String processedUrl = url.substring(7);
        int index = processedUrl.indexOf("/");
        if (index == -1) {
            return processedUrl;
        } else {
            //截取获得hostname
            return processedUrl.substring(0, index);
        }
    }

    /**
     * 将url加入到符合条件的已知url结果集中，加锁的方式将url放入
     *
     * @param url
     */
    private void addUrlToResult(String url) {
        //由于这个数据结构不支持高并发，因此需要加锁
        this.resultLock.lock();
        try {
            this.resultUrls.add(url);
        } finally {
            this.resultLock.unlock();
        }
    }

    /**
     * 返回结果集，加锁的方式确保不会再被改变
     *
     * @return
     */
    private List<String> fetchResultUrls() {
        this.resultLock.lock();
        try {
            return this.resultUrls;
        } finally {
            this.resultLock.unlock();
        }
    }

    /**
     * 添加到待抓取URL链表中，表示这个url符合要求，然后放入url待爬集中表示要启动线程去爬它
     *
     * @param url
     */
    private void addUrlToCrawl(String url) {
        this.crawLock.lock();
        try {
            this.ToCrawlUrls.add(url);
        } finally {
            this.crawLock.unlock();
        }
    }

    /**
     * 从待抓取url中返回并删除一个元素，返回一个供一个线程去爬
     *
     * @return
     */
    private String fetchUrlToCrawl() {
        this.crawLock.lock();
        try {
            return this.ToCrawlUrls.poll();
        } finally {
            this.crawLock.unlock();
        }
    }

    /**
     * 正在执行的工作线程个数增加
     */
    private void incrChore() {
        this.choreCount.incrementAndGet();
    }

    private void decrChore() {
        this.choreCount.decrementAndGet();
    }
    //endregion
}

interface HtmlParser {
    public List<String> getUrls(String url);
}